# -*- coding: utf-8 -*-
#   Copyright (C) 2009, 2013 Rocky Bernstein
#
#   This program is free software: you can redistribute it and/or modify
#   it under the terms of the GNU General Public License as published by
#   the Free Software Foundation, either version 3 of the License, or
#   (at your option) any later version.
#
#   This program is distributed in the hope that it will be useful,
#   but WITHOUT ANY WARRANTY; without even the implied warranty of
#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#   GNU General Public License for more details.
#
#   You should have received a copy of the GNU General Public License
#   along with this program.  If not, see <http://www.gnu.org/licenses/>.
import imp, os, sys

def get_srcdir(level=1):
    """Get directory of caller as an absolute file name. *level* is
    the number of frames to look back.  So for import file which is
    really doing work on behalf of *its* caller, we go back 2.

    NB: f_code.co_filenames and thus this code kind of broken for
    zip'ed eggs circa Jan 2009
    """

    caller = sys._getframe(level)
    filename = caller.f_code.co_filename
    filename = os.path.normcase(os.path.dirname(os.path.abspath(filename)))
    return os.path.realpath(filename)

def get_src(level=1):
    """Get caller filename as an absolute file name. *level* is
    the number of frames to look back.  So for import file which is
    really doing work on behalf of *its* caller, we go back 2.

    NB: f_code.co_filenames and thus this code kind of broken for
    zip'ed eggs circa Jan 2009
    """
    caller = sys._getframe(level)
    filename = caller.f_code.co_filename
    filename = os.path.normcase(os.path.abspath(filename))
    return os.path.realpath(filename)


def get_namespace(top_name, srcdir):
    """Return a compound 'import' string e.g. "a.b.c" based on *srcdir* and
    *top_name*.  Note: we assume packages don't have subdirectories
    with the same name as the package. E.g. pydbg.pydbg or
    pydbg.foo.pydbg is forbidden.
    """
    srcdirs = srcdir.split(os.path.sep)
    if top_name in srcdirs:
        srcdirs.reverse()
        prefix = srcdirs[:srcdirs.index(top_name)]
        prefix.reverse()
        if len(prefix) > 0:
            return "%s.%s" % (top_name, '.'.join(prefix))
        else:
            return top_name
        pass
    return None

def path2abspath(path, call_level):
    '''Turn path into an absolute file name.'''
    alldots = False
    if path is None:
        srcdir = get_srcdir(call_level)
    elif os.path.sep == path[0]:
        srcdir = path
    else:
        # Check for ., .., ...
        if '.' == path[0]:
            alldots = True
            i = 1
            pardir = '.'
            for i in range(1,len(path)):
                if path[i] != '.':
                    path = path[i:]
                    alldots = False
                    break
                pardir = os.path.join(pardir, os.path.pardir)
                pass
            if alldots:
                srcdir = os.path.abspath(os.path.join(get_srcdir(call_level),
                                                      pardir))
            else:
                srcdir = os.path.abspath(os.path.join(get_srcdir(call_level),
                                                      pardir, path))
                pass
            pass
        else:
            srcdir = os.path.abspath(os.path.join(get_srcdir(call_level),
                                                      '.', path))
        pass
    return srcdir

def import_relative(import_name, root=None, top_name=None):
    """Import `import_name' using `root' as the location to start
    looking for it.  If `root' is not given, we'll look starting in
    the directory where the import_relative was issued.
    In contrast to __import__() which this uses, we always return the
    last import module when a compound import (e.g. a.b.c) is given.
    We don't support "from lists", global or local variables here.

    TODO: add a package/namespace parameter for which to add the name under.
    """

    if os.getenv('DEBUG'):
        print("search | import_name: '%s', root: '%s', top_name: '%s'" \
                 ) % (import_name, root, top_name)

    srcdir = path2abspath(root, 3)

    if top_name:
        namespace = get_namespace(top_name, srcdir)
    else:
        namespace = None

    import_modules = import_name.split('.')
    top_module = import_modules[0]
    top_file_path = os.path.join(srcdir, top_module)

    if namespace:
        namespaced_path = "%s.%s" % (namespace, import_name)
    else:
        namespaced_path = import_name

    mod = sys.modules.get(namespaced_path)
    if mod and hasattr(mod, '__file__') and \
        mod.__file__.startswith(top_file_path):
        # success
        sys.modules[namespaced_path] = mod
        result = mod
    else:
        # make use of 'imp.find_module()' and 'imp.load_module()' srcpath
        # parameter to ensure we return the desired module

        # note for compound module names (e.g. 'a.b'), imp.find_module()
        # doesn't seem to work, so we revert to this slightly hacky method
        # of iterating the submodules of '[top_name.]import_name'

        # this doesn't deal with "from .. import .. as .." renaming
        # (which would need to look at __name__)

        # a possible alternative is the hierarchical module import
        # re-implemenation 'knee' see:
        # 'http://docs.python.org/library/imputil.html'
        # it seems to be a translation into Python of its C code

        path_collisions = {}
        prev_mod = None
        path = ""
        fp = None  # file handle
        try:
            for mod_name in import_modules:
                path = path + '.' + mod_name if path else mod_name
                if namespace:
                    namespaced_path = "%s.%s" % (namespace, path)
                else:
                    namespaced_path = path

                if sys.modules.get(namespaced_path):
                    fn = sys.modules[namespaced_path].__file__
                    if fn.startswith(os.path.join(srcdir, top_module)):
                        # success
                        mod = sys.modules.get(namespaced_path)
                        # short-circuit
                        prev_mod = mod
                        continue
                    else:
                        # Temporarily nuke module so it doesn't inferfere
                        # with our searching
                        path_collisions[namespaced_path] = \
                                             sys.modules[namespaced_path]
                        del sys.modules[namespaced_path]

                if not prev_mod:
                    # If any of the following calls raises an exception,
                    # there's a problem we can't handle -- let the caller
                    # handle it.
                    try:
                        fp, pathname, description = \
                            imp.find_module(path, [srcdir])
                    except ImportError:
                        raise ImportError("No module %s found in %s" %
                                          (path, srcdir))
                    try:
                        mod = imp.load_module(namespaced_path, fp,
                                              pathname, description)
                    except (SystemError, ImportError):
                        mod = imp.load_module(path, fp,
                                              pathname, description)
                    sys.modules[namespaced_path] = mod
                else:
                    try:
                        mod = __import__(path, None, None, ['__bogus__'])
                        if namespace:
                            sys.modules[namespaced_path] = mod
                    finally:
                        if namespace and path in sys.modules:
                            del sys.modules[path]

                prev_mod = mod

        finally:
              # in case of an exception, close fp explicitly
              if fp:
                  fp.close()
              if len(path_collisions) > 0:
                # restore any modules temporarily removed from the sys.modules
                # dictionary to facscilite our searching
                for path_partial, mod_orig in path_collisions.items():
                    # leave our target module overriding any original at the
                    # ultimate/complete compound path
                    if path_partial != path:
                        sys.modules[path_partial] = mod_orig
        result = prev_mod

    if os.getenv('DEBUG'):
        print("return | namespaced_path: '%s', mod: '%s'" \
                 ) % (namespaced_path, str(result))

    return result


# Demo it
if __name__=='__main__':
    print(path2abspath('.', 2))
    print(get_namespace('pydbg', '/src/pydbg/pydbg'))
    Mtest = import_relative('test.os2.path', '.', 'import-relative')

    print(get_namespace('pydbg', '/src/pydbg/pydbg/processor/commands'))

    Mimport_relative = import_relative('import_relative')
    print(Mimport_relative)
    print(Mimport_relative.__name__)
    print(Mimport_relative.__file__)
    # The 2nd time around, we should have info cached.
    # Can you say Major Major?
    import_relative2 = Mimport_relative.import_relative('import_relative',
                                                        '.')


    # Originally done with os.path, But nosetest seems to use this.
    os2_path = Mimport_relative.import_relative('os2.path', 'test',
                                                'import_relative')
    print(os2_path)
    print(os2_path.__name__)
    print(os2_path.__file__)
    print(os2_path.me)
    # Warning. I've destroyed the real os.
    pass
